Skip to content

fix(desktop): publish kind:10100 agent profile directory records - #3708

Open
4dlt wants to merge 1 commit into
block:mainfrom
4dlt:pr/agent-directory-publish
Open

fix(desktop): publish kind:10100 agent profile directory records#3708
4dlt wants to merge 1 commit into
block:mainfrom
4dlt:pr/agent-directory-publish

Conversation

@4dlt

@4dlt 4dlt commented Jul 30, 2026

Copy link
Copy Markdown

Summary

The desktop reads a kind:10100 agent-profile directory to decide which agents a client may mention, but nothing has ever written to it. The directory is empty in every deployment, so a managed agent is only mentionable from the machine that runs it, and every eligibility rule that consults the directory is a no-op in practice.

This PR adds the publisher. It publishes an agent's directory record when the agent starts, signed by the agent's own key, and refreshes it as offline when the agent stops.

Fixes half of #3277. No client-visible behaviour changes: this only publishes a record the app already reads.

Why this is separate from the eligibility change

#3277 has an open product question that the mention rule depends on:

Should an agent identity that is a channel member but has no local runtime (the mira fixture case) stay hidden from the picker?

The publisher does not depend on that answer. It keeps mira hidden either way, and it unblocks every directory-based approach in the queue regardless of which rule is chosen. I have the client-side rule working too, but it seemed better to land the uncontested half first rather than add another competing eligibility PR while that question is open. Happy to open the follow-up once the rule is settled.

What is published

An explicit allowlist, because the relay serves this kind to any authenticated member:

name, agent_type, status, respond_to, respond_to_allowlist, channel_ids, channels, capabilities, channel_add_policy

Deliberately absent, each pinned by a test: the agent's nsec, its NIP-OA auth tag, its environment variables, and its runtime/harness configuration.

The record is signed by the agent's own key rather than the owner's. agents_from_events overwrites the content pubkey with the event author, so identity derives from the signature and one agent cannot publish an entry impersonating another. That is also why only the hosting machine can publish.

Three decisions worth reviewing

Published from the runtime start path, not profile reconciliation. A managed agent can start four different ways. reconcile_agent_profile is reached from the UI start command and from boot restore, but the renderer drives start_managed_agent_runtime, which calls start_pair then spawn_agent_child and reconciles nothing. Hooking only reconciliation publishes nothing on the common path, which is what I did first and it silently produced no records. start_pair covers every path.

channel_add_policy is read back and re-sent verbatim. The relay's side effect for this kind writes the field straight into the users table:

// crates/buzz-relay/src/handlers/side_effects.rs, handle_agent_profile
state.db.set_channel_add_policy(tenant.community(), &pubkey_bytes, policy).await?;

Sending a fixed value would therefore reset a deliberate owner_only or nobody to that value on every agent start. The prior record's policy is preserved, falling back to the schema default (anyone) only when there is no prior record, which is a no-op for a never-configured agent.

The publish is not gated on agentManagedProfiles. That experiment decides whether the desktop or the agent owns kind:0 profile metadata. The directory record is discovery, nothing else in the product publishes it, and gating it there leaves remote agents undiscoverable for exactly the users who opted in. kind:0 handling still re-checks the flag and is unchanged. Please push back if that guard was intended to cover discovery too.

Testing

New unit tests in desktop/src-tauri/src/commands/agents_tests.rs:

  • record carries every field a remote client needs to decide mentionability
  • respond_to_allowlist is carried when respond_to is allowlist
  • a stopped agent reports offline
  • four separate tests asserting the record leaks no secret key, no auth attestation, no environment variables, no runtime configuration
  • channel ids are taken from the d tags of kind:39002 membership events, deduped and sorted, skipping malformed events
  • channel names come from the kind:39000 name tag, falling back to the id when metadata is missing
  • channel_add_policy is preserved from a prior record, and falls back to the schema default when absent or malformed

just desktop-tauri-test, just desktop-tauri-clippy and just desktop-tauri-fmt-check pass.

Verified at runtime on this branch, not just in tests

Built from this branch, deployed to a self-hosted relay running Desktop 0.5.2, and queried directly against the relay's event store.

Starting an agent that was stopped (so boot-restore could not have been the publisher) produced a fresh record, which is what confirms the new start_pair hook actually executes rather than being dead code:

{
  "name": "Fizz", "agent_type": "buzz-agent", "status": "online",
  "respond_to": "anyone", "respond_to_allowlist": [],
  "channel_ids": ["<uuid>", "<general-uuid>", "<uuid>"],
  "channels": ["DM", "general", "Welcome"],
  "channel_add_policy": "anyone", "capabilities": []
}

Stopping an agent produced a matching "status":"offline" record two seconds before its restart produced a new "online" one, so both the start and stop paths are exercised. channel_add_policy stayed "anyone" across four consecutive publishes, confirming the read-back does not clobber it under repeated start/stop cycles.

With the companion client-side rule applied locally, a message mentioning a remote agent from a second machine then carries the agent's p tag where previously it did not, and the harness acknowledges within one second. That client-side rule is not part of this PR.

One question I would like a maintainer's view on

The record publishes private and DM channel ids. channel_ids is built from every kind:39002 membership event tagging the agent, with no filtering, so an agent that is in DM channels publishes those channel UUIDs into a record the relay serves to any authenticated member. In my testing an agent's record listed two ["private"]["hidden"]["t","dm"] channels alongside the public one.

I did not filter them, for two reasons: the spec for this work asks for the channels the agent is in without qualification, and relayAgentIsSharedWithUser uses channel overlap to decide invocability, so removing DM channels would break mentioning an agent from inside a DM (an explicit requirement in the linked issue).

But it is a disclosure the field allowlist was otherwise careful to avoid, and it was not called out in the spec. If the preference is to exclude private/hidden channels and accept that DM mentions need a different signal, that is a small change and I am happy to make it. Flagging rather than deciding, since it is a product call.

Related and minor: because DM channels are all literally named DM, the channels array can contain repeated labels for distinct ids. channel_ids remains unique and is what eligibility consumes, so this is presentational only.

Notes for reviewers

  • The channels field currently carries names resolved from kind:39000; where a channel has no readable metadata it falls back to the channel id. channel_ids is the field eligibility logic uses, so this is cosmetic.
  • capabilities is always empty: a managed agent record declares no capability set today, and the client already defaults the field when absent.
  • Directory read cadence is untouched. The existing relaxed poll is deliberate and this PR does not tighten it.
  • No relay-side changes. The record is accepted under the existing write scope for agent-owned replaceable events.

The desktop reads a kind:10100 agent-profile directory to decide which agents
a client may mention, but nothing has ever written to it. The directory is
empty in every deployment, so a managed agent is only ever mentionable from the
machine that runs it, and every eligibility rule that consults the directory is
a no-op in practice. Reported in block#3277.

Publish the record when a managed agent starts, signed by the agent's own key,
and refresh it as offline when the agent stops. kind:10100 is replaceable and
keyed by pubkey, so a refresh is a plain re-publish with no tombstone.

Content is an explicit allowlist, because the relay serves this kind to any
authenticated member: display name, agent type, liveness, respond_to,
respond_to_allowlist, channel ids, channel names, capabilities and
channel_add_policy. The agent's secret key, its NIP-OA auth tag, its
environment variables and its runtime configuration are deliberately absent,
pinned by tests.

Three things worth calling out for review:

Published from the runtime start path, not profile reconciliation. A managed
agent can start four ways. `reconcile_agent_profile` is reached from the UI
start command and from boot restore, but the renderer drives
`start_managed_agent_runtime`, which goes `start_pair` -> `spawn_agent_child`
and reconciles nothing. Publishing only from reconciliation therefore publishes
nothing on the common path. `start_pair` covers every path.

channel_add_policy is read back and re-sent verbatim. The relay's side effect
for this kind writes the field straight into `users.channel_add_policy`, so
sending a fixed value would reset a deliberate `owner_only` or `nobody` to that
value on every agent start. The prior record's policy is preserved, falling
back to the schema default only when no record exists.

The directory publish is not gated on `agentManagedProfiles`. That experiment
decides whether the desktop or the agent owns kind:0 profile metadata. The
directory record is discovery, has no other publisher in the product, and
gating it there leaves remote agents undiscoverable for exactly the users who
opted in. kind:0 handling still re-checks the flag and is unchanged.

Channel names come from the kind:39000 `name` tag rather than the event
content, which is empty on those events.

No client-visible behaviour changes: this only publishes a record the app
already reads. The mention-eligibility rule is deliberately left for a
follow-up, since block#3277 has an open product question about whether a
channel-member agent with no local runtime should stay hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Alvaro de la Torre <a.dltorreruiz@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant